/********************************************************************************
 * Copyright (c) 2011-2017 Red Hat Inc. and/or its affiliates and others
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 1.0 which is available at
 * http://www.eclipse.org/legal/epl-v10.html.
 *
 * SPDX-License-Identifier: EPL-1.0
 ********************************************************************************/
package org.eclipse.ceylon.ide.eclipse.code.editor;

import static org.eclipse.ceylon.ide.eclipse.ui.CeylonPlugin.PLUGIN_ID;
import static org.eclipse.ceylon.ide.eclipse.util.EditorUtil.getActivePage;
import static org.eclipse.ceylon.ide.eclipse.util.Nodes.findNode;
import static org.eclipse.ceylon.ide.eclipse.util.Nodes.getIdentifyingNode;
import static org.eclipse.ceylon.ide.eclipse.util.Nodes.getReferencedExplicitDeclaration;
import static java.util.Collections.emptyList;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.antlr.runtime.CommonToken;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ISynchronizable;
import org.eclipse.jface.text.ITextSelection;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.IAnnotationModelExtension;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.swt.custom.CaretEvent;
import org.eclipse.swt.custom.CaretListener;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IPartListener;
import org.eclipse.ui.IWorkbenchPart;
import org.eclipse.ui.IWorkbenchWindow;
import org.eclipse.ui.IWorkbenchWindowActionDelegate;
import org.eclipse.ui.texteditor.IDocumentProvider;

import org.eclipse.ceylon.compiler.typechecker.tree.Node;
import org.eclipse.ceylon.compiler.typechecker.tree.Tree;
import org.eclipse.ceylon.ide.eclipse.code.hover.DocumentationView;
import org.eclipse.ceylon.ide.eclipse.code.parse.CeylonParseController;
import org.eclipse.ceylon.ide.eclipse.code.parse.TreeLifecycleListener;
import org.eclipse.ceylon.ide.common.util.FindAssignmentsVisitor;
import org.eclipse.ceylon.ide.common.util.FindDeclarationNodeVisitor;
import org.eclipse.ceylon.ide.common.util.FindReferencesVisitor;
import org.eclipse.ceylon.model.typechecker.model.Declaration;
import org.eclipse.ceylon.model.typechecker.model.Referenceable;

public class MarkOccurrencesAction 
        implements IWorkbenchWindowActionDelegate, 
                   CaretListener, 
                   ISelectionChangedListener, 
                   TreeLifecycleListener {
    
    private static final Annotation[] NO_ANNOTATIONS = new Annotation[0];
    
    /**
     * The ID for the kind of annotations created for "mark occurrences"
     */
    public static final String DECLARATION_ANNOTATION = 
            PLUGIN_ID + ".declarationAnnotation";
    public static final String OCCURRENCE_ANNOTATION = 
            PLUGIN_ID + ".occurrenceAnnotation";
    public static final String ASSIGNMENT_ANNOTATION = 
            PLUGIN_ID + ".assignmentAnnotation";

    /**
     * True if "mark occurrences" is currently on/enabled
     */
    private boolean markingEnabled = true;

    private CeylonEditor activeEditor;

    /**
     * The IParseController for the currently-active editor, if any. Could be null
     * if the current editor is not an IMP editor.
     */
    private CeylonParseController parseController;

    /**
     * The document provider for the currently-active editor. Could be null if
     * the current editor is not an IMP editor.
     */
    private IDocumentProvider documentProvider;

    /**
     * The document for the currently-active editor, if any. Could be null if
     * the current editor is not an IMP editor.
     */
    private IDocument document;

    private Annotation[] occurrenceAnnotations;
    
    /**
     * Listens to part-related events from the workbench to monitor when text editors are
     * activated/closed, and keep the necessary listeners pointed at the active editor.
     */
    private final class EditorPartListener 
            implements IPartListener {
        @Override
        public void partActivated(IWorkbenchPart part) {
            if (part instanceof CeylonEditor) {
                setUpActiveEditor((CeylonEditor) part);
                if (documentProvider!=null) {
                    retrieveOccurrenceAnnotations();
                    if (!markingEnabled) {
                        unregisterListeners();
                        removeExistingOccurrenceAnnotations();
                    }
                }
            }
        }
        @Override
        public void partClosed(IWorkbenchPart part) {
            if (part == activeEditor) {
                unregisterListeners();
                activeEditor = null;
                documentProvider = null;
                document = null;
                parseController = null;
                occurrenceAnnotations = null;
                DocumentationView documentationView = 
                        DocumentationView.getInstance();
                if (documentationView!=null) {
                    documentationView.update(null, -1, -1);
                }
            }
        }
        @Override
        public void partBroughtToTop(IWorkbenchPart part) {}
        @Override
        public void partDeactivated(IWorkbenchPart part) {}
        @Override
        public void partOpened(IWorkbenchPart part) {}
    }

    private boolean canRecompute() {
        return !activeEditor.isBackgroundParsingPaused() &&
                !activeEditor.isBlockSelectionModeEnabled() &&
                !activeEditor.isInLinkedMode();
    }
    
    @Override
    public void caretMoved(CaretEvent event) {
        try {
            if (canRecompute()) {
                int offset = 
                        activeEditor.getCeylonSourceViewer()
                            .widgetOffset2ModelOffset(
                                    event.caretOffset);
                recomputeAnnotationsForSelection(offset, 0, 
                        document);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void selectionChanged(SelectionChangedEvent event) {
        try {
            ISelection sel = event.getSelection();
            if (sel instanceof ITextSelection &&
                    canRecompute()) {
                ITextSelection selection = 
                        (ITextSelection) sel;
                recomputeAnnotationsForSelection(
                        selection.getOffset(), 
                        selection.getLength(), 
                        document);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run(IAction action) {
        markingEnabled = action.isChecked();
        if (markingEnabled) {
            CeylonEditor editor = 
                    (CeylonEditor) 
                        getActivePage()
                            .getActiveEditor();
            setUpActiveEditor(editor);
        } 
        else {
            unregisterListeners();
            removeExistingOccurrenceAnnotations();
        }
    }

    private void registerListeners() {
        // getDocumentFromEditor() can return null, but register listeners
        // should only be called when there is an active editor that can
        // be presumed to have a document provider that has document
        IDocument document = getDocumentFromEditor();
        if (document!=null) {
            CeylonSourceViewer sourceViewer = 
                    activeEditor.getCeylonSourceViewer();
            if (sourceViewer!=null) {
                sourceViewer.getTextWidget()
                    .addCaretListener(this);
                sourceViewer.getSelectionProvider()
                    .addSelectionChangedListener(this);
            }
        }
        activeEditor.addModelListener(this);
    }

    private void unregisterListeners() {
        if (activeEditor!=null) {
            CeylonSourceViewer sourceViewer = 
                    activeEditor.getCeylonSourceViewer();
            if (sourceViewer!=null) {
                sourceViewer.getTextWidget()
                    .removeCaretListener(this);
                sourceViewer.getSelectionProvider()
                    .removeSelectionChangedListener(this);
            }
            activeEditor.removeModelListener(this);
        }
    }

    private IDocument getDocumentFromEditor() {
        IDocumentProvider provider = getDocumentProvider();
        return provider==null ? null : 
            provider.getDocument(getEditorInput());
    }

    private void recomputeAnnotationsForSelection(
            int offset, int length, 
            IDocument document) {
        IAnnotationModel annotationModel = 
                documentProvider.getAnnotationModel(
                        getEditorInput());
        Tree.CompilationUnit root = getCompilationUnit();
        if (root == null) {
            // Get this when "selecting" an error message that is shown in the editor view
            // but is not part of the source file; just returning should leave previous
            // markings, if any, as they were (which is probably fine)
            // Also get this when the current AST is null, e.g., as in the event of
            // a parse error
            return;
        }
        List<CommonToken> tokens = 
                activeEditor==null ? null :
                    activeEditor.getParseController()
                        .getTokens();
        Node selectedNode = 
                findNode(root, tokens, 
                        offset, offset+length);
        try {
            List<Node> declarations = 
                    getDeclarationsOf(parseController, 
                            selectedNode);
            List<Node> occurrences = 
                    getOccurrencesOf(parseController, 
                            selectedNode);
            List<Node> assignments = 
                    getAssignmentsOf(parseController, 
                            selectedNode);
//            removeEditedOccurrences(document, declarations);
//            removeEditedOccurrences(document, occurrences);
//            removeEditedOccurrences(document, assignments);
            int capacity = 
                    declarations.size() + 
                    occurrences.size() + 
                    assignments.size();
            Map<Annotation,Position> annotationMap = 
                    new HashMap<Annotation,Position>
                        (capacity);
            addPositionsToAnnotationMap(
                    convertRefNodesToPositions(declarations), 
                    DECLARATION_ANNOTATION, document, 
                    annotationMap);
            addPositionsToAnnotationMap(
                    convertRefNodesToPositions(occurrences), 
                    OCCURRENCE_ANNOTATION, document, 
                    annotationMap);
            addPositionsToAnnotationMap(
                    convertRefNodesToPositions(assignments), 
                    ASSIGNMENT_ANNOTATION, document, 
                    annotationMap);
            placeAnnotations(annotationMap, annotationModel);
        } 
        catch (Exception e) {
            e.printStackTrace();
        }
        DocumentationView documentationView = 
                DocumentationView.getInstance();
        if (documentationView!=null) {
            documentationView.update(activeEditor, 
                    offset, length);
        }
    }

    /*private static void removeEditedOccurrences(
            IDocument document, 
            List<Node> occurrences)
            throws BadLocationException {
        for (Iterator<Node> it=occurrences.iterator(); 
                it.hasNext();) {
            Node next = it.next();
            if (next != null) {
                CommonToken tok = 
                        (CommonToken) 
                            next.getToken();
                if (tok != null) {
                    try {
                        int start = tok.getStartIndex();
                        int stop = tok.getStopIndex();
                        int len = stop-start+1;
                        String docText = 
                                document.get(start, len);
                        if (docText.startsWith("\\")) {
                            docText = docText.substring(2);
                        }
                        if (!docText.equals(tok.getText())) {
                            it.remove();
                        }
                    }
                    catch (BadLocationException e) {
                        it.remove();
                    }
                }
            }
        }
    }*/

    private void addPositionsToAnnotationMap(
            Position[] positions, String type, 
            IDocument document, 
            Map<Annotation, Position> annotationMap) {
        for(int i=0; i<positions.length; i++) {
            Position position = positions[i];
            try { // Create & add annotation
                String message = 
                        document.get(position.offset, 
                                position.length);
                annotationMap.put(
                        new Annotation(type, false, message), 
                        position);
            } 
            catch (BadLocationException ex) {
                continue; // skip apparently bogus position
            }
        }
    }

    private void placeAnnotations(
            Map<Annotation,Position> annotationMap, 
            IAnnotationModel annotationModel) {
        if (annotationModel==null || 
                (occurrenceAnnotations==null || 
                 occurrenceAnnotations.length==0) && 
                annotationMap.isEmpty()) {
            return;
        }
        synchronized (getLockObject(annotationModel)) {
            if (annotationModel 
                    instanceof IAnnotationModelExtension) {
                IAnnotationModelExtension ext = 
                        (IAnnotationModelExtension) 
                            annotationModel;
                ext.replaceAnnotations(occurrenceAnnotations, 
                        annotationMap);
            } 
            else {
                removeExistingOccurrenceAnnotations();
                Iterator<Map.Entry<Annotation,Position>> iter = 
                        annotationMap.entrySet()
                            .iterator();
                while (iter.hasNext()) {
                    Map.Entry<Annotation,Position> mapEntry = 
                            iter.next();
                    Annotation ann = mapEntry.getKey();
                    Position pos = mapEntry.getValue();
                    annotationModel.addAnnotation(ann, pos);
                }
            }
            occurrenceAnnotations = 
                    (Annotation[]) 
                        annotationMap.keySet()
                            .toArray(NO_ANNOTATIONS);
        }
    }

    private void retrieveOccurrenceAnnotations() {
        IAnnotationModel annotationModel = 
                documentProvider.getAnnotationModel(
                        getEditorInput());
        // Need to initialize the set of pre-existing annotations in order
        // for them to be removed properly when new occurrences are marked
        if (annotationModel != null) {
            Iterator<Annotation> annotationIterator = 
                    annotationModel.getAnnotationIterator();
            List<Annotation> annotationList = 
                    new ArrayList<Annotation>();
            while (annotationIterator.hasNext()) {
                // SMS 23 Jul 2008:  added test for annotation type
                Annotation ann = annotationIterator.next();
                String type = ann.getType();
                if (type.startsWith(DECLARATION_ANNOTATION) ||
                    type.startsWith(OCCURRENCE_ANNOTATION) ||
                    type.startsWith(ASSIGNMENT_ANNOTATION)) {
                    annotationList.add(ann);
                }
            }
            occurrenceAnnotations = 
                    annotationList.toArray(NO_ANNOTATIONS);
        }
    }

    void removeExistingOccurrenceAnnotations() {
        // RMF 6/27/2008 - If we've come up in an empty workspace, there won't be an active editor
        if (activeEditor == null)
            return;
        // RMF 6/27/2008 - Apparently partActivated() gets called before the editor is initialized
        // (on MacOS?), and then we can't properly initialize this MarkOccurrencesAction instance.
        // When that happens, fDocumentProvider will be null. Initialization needs a fix for that,
        // rather than this simple-minded null guard.
        if (documentProvider == null)
            return;
        IAnnotationModel annotationModel = 
                documentProvider.getAnnotationModel(
                        getEditorInput());
        if (annotationModel==null || 
                occurrenceAnnotations==null)
            return;

        synchronized (getLockObject(annotationModel)) {
            if (annotationModel 
                    instanceof IAnnotationModelExtension) {
                IAnnotationModelExtension ext = 
                        (IAnnotationModelExtension) 
                            annotationModel;
                ext.replaceAnnotations(occurrenceAnnotations, null);
            } 
            else {
                for (int i=0, 
                        length=occurrenceAnnotations.length; 
                        i<length; 
                        i++) {
                    annotationModel.removeAnnotation(
                            occurrenceAnnotations[i]);
                }
            }
            occurrenceAnnotations= null;
        }
    }

    private Position[] convertRefNodesToPositions(List<Node> refs) {
        List<Position> positions = new ArrayList<>(refs.size());
        for (Iterator<Node> iter=refs.iterator(); 
                iter.hasNext(); 
                ) {
            Node node = iter.next();
            Node identifyingNode = getIdentifyingNode(node);
            if (identifyingNode != null) {
                positions.add(new Position(
                        identifyingNode.getStartIndex(), 
                        identifyingNode.getDistance()));
            }
        }
        Position[] result = new Position[positions.size()];
        return positions.toArray(result);
    }

    private Tree.CompilationUnit getCompilationUnit() {
        // Do NOT compute fCompilationUnit conditionally based
        // on the AST being null; that causes problems when switching
        // between editor windows because the old value of the AST
        // will be retained even after the new window comes up, until
        // the text in the new window is parsed.  For now just
        // get the current AST (but in the future do something more
        // sophisticated to avoid needless recomputation but only
        // when it is truly needless).
        return parseController.getLastCompilationUnit();
    }

    private IEditorInput getEditorInput() {
        return activeEditor.getEditorInput();
    }

    private IDocumentProvider getDocumentProvider() {
        documentProvider = 
                activeEditor.getDocumentProvider();
        return documentProvider;
    }

    private void setUpActiveEditor(CeylonEditor textEditor) {
        unregisterListeners();
        if (textEditor == null) {
            return;
        }
        activeEditor = textEditor;
        document = getDocumentFromEditor();
        parseController = 
                activeEditor.getParseController();

        if (parseController == null) {
            return;
        }

        registerListeners();

        ISelection selection = 
                activeEditor.getSelectionProvider()
                        .getSelection();
        if (selection instanceof ITextSelection) {
            ITextSelection textSelection = 
                    (ITextSelection) selection;
            recomputeAnnotationsForSelection(
                    textSelection.getOffset(), 
                    textSelection.getLength(), 
                    document);
        }
    }

    private Object getLockObject(IAnnotationModel annotationModel) {
        if (annotationModel instanceof ISynchronizable) {
            ISynchronizable sync = 
                    (ISynchronizable) annotationModel;
            return sync.getLockObject();
        }
        else {
            return annotationModel;
        }
    }

    @Override
    public void selectionChanged(IAction action, 
            ISelection selection) {}

    @Override
    public void dispose() {
        unregisterListeners();
    }

    @Override
    public void init(IWorkbenchWindow window) {
        window.getActivePage()
            .addPartListener(new EditorPartListener());
    }

    @Override
    public void update(CeylonParseController parseController,
            IProgressMonitor monitor) {
        if (activeEditor!=null) {
//            synchronized (activeEditor) {
                if (!activeEditor.isBackgroundParsingPaused() &&
                        !activeEditor.isInLinkedMode()) {
                    try {
                        activeEditor.getEditorSite()
                            .getShell()
                            .getDisplay()
                            .asyncExec( 
                                new Runnable() {
                            @Override
                            public void run() {
                                if (activeEditor!=null) {
                                    IRegion selection = 
                                            activeEditor.getSelection();
                                    recomputeAnnotationsForSelection(
                                            selection.getOffset(), 
                                            selection.getLength(), 
                                            document);
                                }
                            }
                        });
                    } 
                    catch (Exception e) {
                        e.printStackTrace();
                    }
                }
//            }
        }
    }

    @Override
    public Stage getStage() {
        return Stage.TYPE_ANALYSIS;
    }
    
    private List<Node> getDeclarationsOf(
            CeylonParseController controller, Node node) {
        if (controller.getStage().ordinal() 
                >= getStage().ordinal()) {
            // Check whether we even have an AST in which to find occurrences
            Tree.CompilationUnit root = 
                    controller.getLastCompilationUnit();
            if (root!=null) {
                Referenceable declaration = 
                        getReferencedExplicitDeclaration(
                                node, root);
                if (declaration!=null) {
                    List<Node> occurrences = 
                            new ArrayList<Node>();
                    FindReferencesVisitor frv = 
                            new FindReferencesVisitor(
                                    declaration);
                    FindDeclarationNodeVisitor fdv = 
                            new FindDeclarationNodeVisitor(
                                    frv.getDeclaration());
                    root.visit(fdv);
                    Tree.StatementOrArgument decNode = 
                            fdv.getDeclarationNode();
                    if (decNode!=null) {
                        occurrences.add(decNode);
                    }
                    return occurrences;
                }
            }
        }
        return emptyList();
    }
    
    private List<Node> getOccurrencesOf(
            CeylonParseController controller, Node node) {
        if (controller.getStage().ordinal() 
                >= getStage().ordinal()) {
            // Check whether we even have an AST in which to find occurrences
            Tree.CompilationUnit root = 
                    controller.getLastCompilationUnit();
            if (root!=null) {
                Referenceable declaration = 
                        getReferencedExplicitDeclaration(
                                node, root);
                if (declaration!=null) {
                    List<Node> occurrences = 
                            new ArrayList<Node>();
                    FindReferencesVisitor frv = 
                            new FindReferencesVisitor(
                                    declaration);
                    root.visit(frv);
                    occurrences.addAll(frv.getReferenceNodeSet());
                    return occurrences;
                }
            }
        }
        return emptyList();
    }
    
    private List<Node> getAssignmentsOf(
            CeylonParseController controller, Node node) {
        // Check whether we even have an AST in which to find occurrences
        Tree.CompilationUnit root = 
                controller.getLastCompilationUnit();
        if (root!=null) {
            Referenceable declaration = 
                    getReferencedExplicitDeclaration(
                            node, root);
            if (declaration instanceof Declaration) {
                List<Node> occurrences = 
                        new ArrayList<Node>();
                FindAssignmentsVisitor frv = 
                        new FindAssignmentsVisitor(
                                (Declaration) declaration);
                root.visit(frv);
                occurrences.addAll(frv.getAssignmentNodeSet());
                return occurrences;
            }
        }
        return emptyList();
    }
    
}